---
title: 在 React 中进行声明式思考
description: 关于在 React 中使用声明式和状态驱动方法的指南，其中包含实际示例，如待办事项列表，以说明构建动态、可维护的 UI
---

React 的核心原则之一是其声明性。React 允许你定义 UI 应该根据当前状态呈现的样子，而不是手动逐步更新 DOM（命令式编程），它会为你处理更新。这种方法使 UI 开发更具可预测性、可扩展性，并且更容易推理。

## 声明式 UI 与命令式 UI

在命令式编程中，你给出关于事情应该如何发生的明确指令。DOM API 本质上是命令式的。当操作 DOM 时，这通常意味着选择元素并手动修改它们。

```js
const button = document.createElement('button');
button.textContent = 'Click me';
button.style.backgroundColor = 'blue';
document.body.appendChild(button);

button.addEventListener('click', () => {
  button.style.backgroundColor = 'red';
  alert('Button clicked!');
});
```

请注意，每个操作都被明确定义：创建按钮、设置样式、将其附加到 DOM 以及响应事件更改属性。

另一方面，声明式编程侧重于描述期望的结果，而不是详细说明如何实现它。React 组件允许我们声明 UI 应该是什么样子，当状态改变时，React 会负责更新它。

```jsx
function App() {
  const [color, setColor] = React.useState('blue');

  return (
    <button style={{ backgroundColor: color }} onClick={() => setColor('red')}>
      Click me
    </button>
  );
}
```

在这个例子中，我们根据 `color` 状态定义了按钮的 UI。当状态改变时，React 会自动更新 DOM，而无需我们手动管理它。

### 类比：命令式与声明式

想想制作一杯咖啡：

* **命令式：**“拿一个马克杯，倒入热水，加入咖啡，搅拌，然后享用”
* **声明式：**“我想要一杯咖啡”

在声明式方法中，执行的细节被抽象掉了。你描述最终结果，然后一个系统（例如咖啡机、咖啡师或 React）确保它正确发生。

声明式编程的好处在上面的按钮示例中可能不太明显，因为它很小。

让我们使用一个稍微复杂一点的待办事项列表示例，该列表允许添加、删除和完成任务。UI 应该显示任务列表和任务总数以及已完成的任务。

使用命令式方法（例如，使用原始 JavaScript 和 DOM API），每次交互都需要手动查找、更新和重新渲染元素。

以下用户操作将需要这些 DOM 操作：

1. **添加任务**：
   1. 将新任务追加到现有列表中
   2. 清空输入内容
   3. 为新添加的任务添加任务完成事件监听器
   4. 增加总任务数
2. **完成任务**：
   1. 更新任务以显示已完成状态
   2. 修改已完成任务的数量
3. **删除任务**：
   1. 从列表中删除任务
   2. 减少总任务数
   3. 如果任务已完成，则减少已完成任务的数量

使用命令式方法，要使 UI 与状态保持同步要困难得多，因为您必须记住要修改的相关区域。当引入新功能时，用命令式编写的逻辑会变得更难阅读和跟踪。这对可维护性不利！

使用声明式方法（如 React），您可以简单地描述应根据更新后的任务状态显示的 UI，React 将计算出必要的命令式 DOM 操作，以将当前 UI 演变为预期的 UI。

可能存在无数个可能的当前 UI – React 如何计算出要进行的正确命令式 DOM 操作？ 这就是 React 的虚拟 DOM 和协调过程发挥作用的地方。 React 比较/区分当前 UI 表示和新的 UI 表示，并生成必要的 DOM 操作列表。

声明式 UI 是几乎所有现代 UI 框架都采用的方法，因为它相对于命令式方法具有压倒性的优势。

在 react.dev 上进一步阅读：[声明式 UI 与命令式 UI 的比较](https://react.dev/learn/reacting-to-input-with-state#how-declarative-ui-compares-to-imperative)

## 如何以声明方式思考 UI

以声明方式思考需要将您的重点从如何更新 UI 转移到 UI 在任何给定时刻应该是什么。

让我们使用与上面相同的待办事项列表示例，并演示声明式编程如何更好。 我们不手动操作 DOM（命令式编程），而是根据状态定义 UI 应该如何显示，并让 React 负责渲染。

为了使事情更复杂一些，待办事项列表支持过滤（全部、已完成、未完成）。

### 1. 识别组件中的视觉状态

待办事项列表有几种可能的 UI 状态：

* 输入字段，可以接受用户的文本输入
* 任务列表，可以为空或非空
* 每个任务可以完成或未完成
* 任务筛选器的选择器和所选选项
* 任务列表可以被过滤（全部、活动、已完成）

### 2. 确定触发状态更改的操作

接下来，我们定义影响状态的操作：

* 添加任务
* 完成任务
* 删除任务
* 过滤任务（显示全部、活动或已完成）

这些操作将修改状态，React 将相应地自动重新渲染 UI。

### 3. 设计一个最小结构来表示状态

接下来，我们需要设计一个结构来捕获 UI 中的状态数据。

这是一个可能的方案：

```js
const [tasks, setTasks] = useState([
  { id: 1, title: '学习 React' },
  { id: 2, title: '构建一个项目' },
]);
const [completedTasks, setCompletedTasks] = useState([
  { id: 1, title: '学习 React' },
]);
const [incompleteTasks, setIncompleteTasks] = useState([
  { id: 2, title: '构建一个项目' },
]);
const [filter, setFilter] = useState('all'); // all | active | completed
```

然而，在这种设计中，存在一些数据重复。任务是 `tasks` 和 `completedTasks` 或 `incompleteTasks` 的一部分，添加/删除任务将涉及修改多个任务数组。这种状态设计不太好。

一些改进方法：

1. `incompleteTasks` 状态是多余的，因为如果一个任务不在 `completedTasks` 中，那么它就是未完成的。但是，当线性扫描 `completedTasks` 数组以检查每个任务是否已完成时，效率会很低
2. 我们可以对 `completedTasks` 使用一个 `Set`，它只存储任务 ID。这样，任务标题就不必重复，并且查找任务的完成状态也很有效。但是，当已完成的任务被删除时，我们仍然需要记住修改两个地方。

我们可以跟踪任务本身的完成状态，而不是存储需要同步的多个独立状态片段。这是一个最小且结构化的状态表示：

```js
const [tasks, setTasks] = useState([
  { id: 1, title: 'Learn React', completed: false },
  { id: 2, title: 'Build a project', completed: true },
]);

const [filter, setFilter] = useState('all'); // all | active | completed
```

使用此结构：

* `tasks` 是一个数组，其中每个任务都有一个 `id`、`title` 和 `completed` 状态
* `filter` 确定要显示的任务

为了避免状态冗余，我们可以从 `tasks` 派生已完成的任务，而不是保留一个单独的 `completedTasks` 数组。

这里需要考虑权衡。虽然状态现在已合并，但切换任务完成状态现在将需要克隆整个 `tasks` 数组，而如果我们对 `completedTasks` 使用 `Set`，则不需要这样做。

### 4. 在事件处理程序中调用操作

操作是响应两种事件而触发的：

* **用户事件**：用户直接执行的操作，例如单击按钮、在输入字段中键入或从下拉列表中选择一个选项
* **后台事件**：在没有直接用户交互的情况下触发的操作，例如 API 响应、计时器和间隔、WebSocket 实时更新

对于手头的待办事项列表，我们只需要关心用户事件。

但是，建议为每个操作编写一个函数，并在每个操作函数中调用状态设置器。这是因为相同的操作可以从 UI 上的许多地方甚至在后台触发。一个例子是视频播放器，用户可以按“暂停”按钮或按空格键来暂停视频。

在这些操作函数中集中状态更新逻辑将有助于保持代码的可维护性。

### 完整示例

现在，让我们实现上面学到的内容。

```jsx
import { useState } from 'react';

function TodoApp() {
  const [tasks, setTasks] = useState([]);
  const [filter, setFilter] = useState('all');
  const [taskInput, setTaskInput] = useState('');

  // Add a task
  function addTask() {
    if (taskInput.trim() === '') {
      return;
    }
    const newTask = { id: Date.now(), text: taskInput, completed: false };
    setTasks([...tasks, newTask]);
    setTaskInput('');
  }

  // Toggle task completion
  function toggleTask(id) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task,
      ),
    );
  }

  // Delete a task
  function deleteTask(id) {
    setTasks(tasks.filter((task) => task.id !== id));
  }

  // Get tasks based on filter
  const filteredTasks = tasks.filter((task) => {
    if (filter === 'active') {
      return !task.completed;
    }

    if (filter === 'completed') {
      return task.completed;
    }

    return true; // "all" case
  });

  return (
    <div>
      <h2>Todo List</h2>
      <input
        value={taskInput}
        onChange={(e) => setTaskInput(e.target.value)}
        placeholder="Add a new task..."
      />
      <button onClick={addTask}>Add</button>
      <div>
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('active')}>Active</button>
        <button onClick={() => setFilter('completed')}>Completed</button>
      </div>
      <ul>
        {filteredTasks.map((task) => (
          <li
            key={task.id}
            style={{
              textDecoration: task.completed ? 'line-through' : 'none',
            }}>
            {task.text}
            <button onClick={() => toggleTask(task.id)}>
              {task.completed ? 'Undo' : 'Complete'}
            </button>
            <button onClick={() => deleteTask(task.id)}>❌</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;
```

这个待办事项列表是声明式的，因为：

* **UI 自动更新**：当 `tasks` 或 `filter` 更改时，React 会相应地重新渲染 UI
* **没有手动 DOM 操作**：无需选择元素、手动删除任务或通过 `document.querySelector` 更新样式。
* **最小且结构化的状态**：UI 源自**单一事实来源** (`tasks` 和 `filter`)
* **事件处理程序更新状态，而不是 DOM**：函数 `addTask`、`toggleTask` 和 `deleteTask` 仅更新状态，React 处理重新渲染。

此示例展示了声明式思维如何简化 React 中的 UI 开发：

1. 识别 **UI 状态**
2. 确定 **状态更改操作/操作**
3. 设计一个 **最小状态结构**
4. 使用 **事件处理程序** 更新状态

我们让 React **响应** 状态变化，而不是微观管理 DOM 更新，这使得我们的代码更简洁、更具可扩展性，并且更易于维护。

在 React 中以声明式方式思考意味着从一步一步的、命令驱动的思维模式转变为状态驱动的方法。 通过专注于根据状态定义 UI 应该是什么，您可以创建更具可读性、可预测性和可维护性的组件。

拥抱这种范式可以更容易地管理复杂的 UI，并让 React 承担起确定如何有效地更新 DOM 的繁重工作。

## 面试需要了解的内容

* **以声明方式思考并设计组件**：给定一个 UI 和需求，您应该能够以声明式方式思考，以定义各种必要的组件、道具、状态、更改状态的操作，并将它们全部连接在一起。

## 练习题

**编码**：

* [Tweet](/questions/user-interface/tweet/react?framework=react\&tab=coding)
* [进度条](/questions/user-interface/progress-bar/react?framework=react\&tab=coding)
* [进度条](/questions/user-interface/progress-bars/react?framework=react\&tab=coding)
* [文件资源管理器](/questions/user-interface/file-explorer/react?framework=react\&tab=coding)
* [文件资源管理器 II](/questions/user-interface/file-explorer-ii/react?framework=react\&tab=coding)
* [文件资源管理器 III](/questions/user-interface/file-explorer-iii/react?framework=react\&tab=coding)
